/**
*@file	Test kernel driver module
*
*
*@date	2008.9.24
*/

#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>

#include <linux/kernel.h>	/* printk() */
#include <linux/slab.h>		/* kmalloc() */
#include <linux/fs.h>		/* everything... */
#include <linux/errno.h>	/* error codes */
#include <linux/types.h>	/* size_t */
#include <linux/fcntl.h>	/* O_ACCMODE */
#include <linux/cdev.h>
#include <linux/poll.h>

#include <asm/system.h>		/* cli(), *_flags */
#include <asm/uaccess.h>	

#include "hello.h"

#define HELLO_DEBUG
#ifdef HELLO_DEBUG
	#define hello_printk(fmt,args...)	printk(KERN_DEBUG fmt,##args)
#else
	#define hello_printk(fmt,args...)
#endif

int hello_major=HELLO_MAJOR;
int hello_minor=0;
int hello_nr=4;
int quatum=QUANTUM;
int qset=QSET;

//EXPORT_SYMBOL(hello_major);
//EXPORT_SYMBOL(hello_minor);
module_param(hello_major, int, S_IRUGO);
module_param(hello_minor, int, S_IRUGO);
module_param(hello_nr,int,S_IRUGO); 
module_param(quatum,int,S_IRUGO);
module_param(qset,int,S_IRUGO);

struct hello_dev  *hello_deves;

int hello_trim(struct hello_dev  *hello_dev)
{
	struct hello_qset *next,*dptr;
	int qset=hello_dev->qset_size;
	int i;
	
	for (dptr=hello_dev->data;dptr;dptr=next){
		if (dptr->data){
			for (i=0;i<qset;i++)
				kfree(dptr->data[i]);
			kfree(dptr->data);
			dptr->data=NULL;
			
		}
		next=dptr->next;
		kfree(dptr);
	}
	hello_dev->data=NULL;
	hello_dev->size=0;
	hello_dev->quantum_size=quatum;
	hello_dev->qset_size=qset;

	return 0;
}

int hello_open(struct inode* inode,struct file *filp)
{
	struct hello_dev *hdev;
	
	hello_printk("info@line %d:Enter hello_open\n",__LINE__);
	hdev=container_of(inode->i_cdev,struct hello_dev,dev);
	filp->private_data=hdev;
	
	if((filp->f_flags & O_ACCMODE) ==O_WRONLY){
		if (down_interruptible(&hdev->sem))	return -ERESTARTSYS;		
		hello_trim(hdev);
		up(&hdev->sem);
	}
	
	return  0;
}

struct hello_qset* hello_follow(struct hello_dev *hdev,int n)
{
	struct hello_qset *qs=hdev->data;

	if (!qs){
		qs=hdev->data=kmalloc(sizeof(struct hello_qset),GFP_KERNEL);
		if(qs==NULL)
			return NULL;
		memset(qs,0,sizeof(struct hello_qset));
	}

	while(n--){
		if(!qs->next){
			qs->next=kmalloc(sizeof(struct hello_qset),GFP_KERNEL);
			if(qs->next==NULL)
				return NULL;
			memset(qs->next , 0 , sizeof(struct hello_qset));
		}
		qs=qs->next;
		continue;
	}

	return qs;
}


int hello_release(struct inode *inode,struct file *filp)
{
	return 0;
}

ssize_t hello_read(struct file *filp,char __user *buf,size_t count,loff_t *f_pos)
{
	struct hello_dev *hdev;
	struct hello_qset *dptr;
	int qutum_size,qset_size,item_size;
	int qset_item,qset_pos,qutum_pos,rest;
	ssize_t retval=0;

	hdev=filp->private_data;
	qutum_size=hdev->quantum_size;
	qset_size=hdev->qset_size;
	item_size=qset_size*qutum_size;

	if(down_interruptible(&hdev->sem))
		return -ERESTARTSYS;
	if(*f_pos >= hdev->size)
		goto out;
	if(*f_pos+count>hdev->size)
		count=hdev->size-*f_pos;
	/** 
	The value of *f_pos must be changed to long or unsigned long, otherwise the follow warning will occurs and modules can't insert.
	
	WARNING: "__moddi3" [/root/svngoogle/iceman-ubuntu/cdev_driver/hello.ko] undefined!
	WARNING: "__divdi3" [/root/svngoogle/iceman-ubuntu/cdev_driver/hello.ko] undefined!	
	*/
	while(hdev->cur_len==0){
	  up(&hdev->sem);
	  if(filp->f_flags&O_NONBLOCK){
	    hello_printk("info@file %s line %d:No readable data,return in NONBLOCK mode\n",__FILE__,__LINE__);
	    return -EAGAIN;
	  }
	  hello_printk("info@file %s line %d:Going to sleep.\n",__FILE__,__LINE__);
	  if(wait_event_interruptible(hdev->r_wait,(hdev->cur_len!=0)))
	    return -ERESTARTSYS;
	  
	}
	
	qset_item=(long)*f_pos/item_size;	
	rest=(long)*f_pos%item_size;
	qset_pos=rest/qutum_size;
	qutum_pos=rest%qutum_size;

	dptr=hello_follow(hdev, qset_item);
	if(dptr==NULL || !dptr->data ||!dptr->data[qset_pos])
		goto out;
	if(count>qutum_size-qutum_pos) 
		count=qutum_size-qutum_pos;
	if(copy_to_user(buf,dptr->data[qset_pos]+qutum_pos,count)){
		retval=-EFAULT;
		goto out;
	}
	*f_pos+=count;
	retval=count;
out:
	up(&hdev->sem);
		
	return retval;
		
}

ssize_t hello_write(struct file *filp,const char __user *buf,size_t count,loff_t *f_pos)
{
	struct hello_dev *dev = filp->private_data;
	struct hello_qset *dptr;
	int quantum = dev->quantum_size, qset = dev->qset_size;
	int itemsize = quantum * qset;
	int item, s_pos, q_pos, rest;
	ssize_t retval = -ENOMEM; /* value used in "goto out" statements */

	if (down_interruptible(&dev->sem))
		return -ERESTARTSYS;
	
	/* find listitem, qset index and offset in the quantum */
	item = (long)*f_pos / itemsize;
	rest = (long)*f_pos % itemsize;
	s_pos = rest / quantum; q_pos = rest % quantum;
	hello_printk("info@line:%d  itme=%d,rest=%d,s_pos=%d,q_pos=%d\n",__LINE__,item,rest,s_pos,q_pos);
	/* follow the list up to the right position */
	dptr = hello_follow(dev, item);
	if (dptr == NULL){
		hello_printk("err@line %d\n",__LINE__);
		goto out;
	}
	if (!dptr->data) {
		dptr->data = kmalloc(qset * sizeof(char *), GFP_KERNEL);
		hello_printk("info@line %d: dptr->data=%p\n",__LINE__,dptr->data);
		if (!dptr->data){
			hello_printk("err@line %d\n",__LINE__);
			goto out;
		}
		memset(dptr->data, 0, qset * sizeof(char *));
	}
	if (!dptr->data[s_pos]) {
		dptr->data[s_pos] = kmalloc(quantum*sizeof(char*), GFP_KERNEL);
		hello_printk("info@line %d: dptr->data[%d]=%p\n",__LINE__,s_pos,dptr->data);
		if (!dptr->data[s_pos])
			goto out;
	}
	/* write only up to the end of this quantum */
	if (count > quantum - q_pos)
		count = quantum - q_pos;

	if (copy_from_user(dptr->data[s_pos]+q_pos, buf, count)) {
		retval = -EFAULT;
		goto out;
	}
	*f_pos += count;
	retval = count;

        /* update the size */
	if (dev->size < *f_pos)
		dev->size = *f_pos;

  out:
	up(&dev->sem);
	return retval;
	
}

int hello_ioctl(struct inode *node,struct file* filp ,unsigned int cmd,unsigned long arg)
{
  int ret=0,tmp;
  int err=0;

  if(_IOC_TYPE(cmd)!=HELLO_IOC_MAGIC)  return -ENOTTY;
  if(_IOC_NR(cmd)>HELLO_IOC_MAXNR)  return -ENOTTY;

  if(_IOC_DIR(cmd)&_IOC_READ)
    err=!access_ok(VERIFY_WRITE,(void __user*)arg,_IOC_SIZE(cmd));
  else if(_IOC_DIR(cmd)&_IOC_WRITE)
    err=!access_ok(VERIFY_READ,(void __user*)arg,_IOC_SIZE(cmd));
  if(err)
    return -EFAULT;
  
  switch(cmd){
  case HELLO_IOCRESET:
    quatum=QUANTUM;
    qset=QSET;
    break;
  case HELLO_IOCSQUANTUM:
    if(!capable(CAP_SYS_ADMIN)) return -EPERM;
    ret=__get_user(quatum,(int __user*)arg);
    break;
  case HELLO_IOCSQSET:
    if(!capable(CAP_SYS_ADMIN)) return -EPERM;
    ret=__get_user(qset,(int __user*)arg);
    break;
  case HELLO_IOCTQUANTUM:
    if(!capable(CAP_SYS_ADMIN)) return -EPERM;
    quatum=arg;
    break;
  case HELLO_IOCTQSET:
    if(!capable(CAP_SYS_ADMIN)) return -EPERM;
    qset=arg;
    break;
  case HELLO_IOCGQUANTUM:
    ret=__put_user(quatum,(int __user*)arg);
    break;
  case HELLO_IOCGQSET:
    ret=__put_user(qset,(int __user*)arg);
    break;
  case HELLO_IOCQQUANTUM:
    return quatum;
  break;
  case HELLO_IOCQQSET:
    return qset;
  break;
  case HELLO_IOCXQUANTUM:
    if(!capable(CAP_SYS_ADMIN)) return -EPERM;
    tmp=quatum;
    err=__get_user(quatum,(int __user*)arg);
    if(!err)
      __put_user(tmp,(int __user*)arg);
  case HELLO_IOCXQSET:
    if(!capable(CAP_SYS_ADMIN)) return -EPERM;
    tmp=qset;
    err=__get_user(qset,(int __user*)arg);
    if(!err)
      __put_user(qset,(int __user*)arg);
  case HELLO_IOCHQUANTUM:
    if(!capable(CAP_SYS_ADMIN)) return -EPERM;
    tmp=quatum;
    quatum=arg;
    return tmp;
  case HELLO_IOCHQSET:
    if(!capable(CAP_SYS_ADMIN)) return -EPERM;
    tmp=qset;
    qset=arg;
    return tmp;
    
  default:
    return -ENOTTY;
  }
  
  return ret;
}

loff_t hello_llseek(struct file *filp,loff_t offset,int whence)
{
  loff_t new_pos;
  struct hello_dev *dev=filp->private_data;

  switch(whence){
  case SEEK_SET:
    new_pos=offset;
    break;
  case SEEK_CUR:
    new_pos=filp->f_pos+offset;
    break;
  case SEEK_END:
    new_pos=dev->size+offset;
    break;
  default:
    return -EINVAL;
  }
  if(new_pos<0) return -EINVAL;
  filp->f_pos=new_pos;
  return new_pos;
}

static unsigned int hello_poll(struct file *filp,poll_table *wait)
{
  unsigned int mask;
  struct hello_dev *dev=filp->private_data;

  down(&dev->sem);

  poll_wait(filp,&dev->r_wait,wait);
  poll_wait(filp,&dev->w_wait,wait);
  if(dev->cur_len != 0)
    mask=POLLIN | POLLRDNORM;
  if(dev->cur_len != dev->size)
     mask=POLLOUT | POLLWRNORM;
  up(&dev->sem);
  
  return mask;
}

struct file_operations hello_ops={
	.owner=THIS_MODULE,
	.read=hello_read,
	.write=hello_write,
	.open=hello_open,
	.release=hello_release,	
	.ioctl=hello_ioctl,
	.llseek=hello_llseek,
	.poll=hello_poll,
};

static void hello_setup_cdev(struct hello_dev *hdev,int index)
{
	int ret;
	dev_t dev_no;
		
	dev_no=MKDEV(hello_major,hello_minor+index);
	cdev_init(&hdev->dev, &hello_ops);
	
	hdev->dev.owner=THIS_MODULE;
	ret=cdev_add(&hdev->dev, dev_no, 1);
	if(ret)
		hello_printk( "Err %d @add char device  %d" ,ret,index);
}

void hello_cleanup_module(void)
{
	int i;
	dev_t dev_no=MKDEV(hello_major,hello_minor);
	hello_printk("Hello module clean up\n");
	if(hello_deves){
		for(i=0;i<hello_nr;i++){
			hello_trim(hello_deves+i);
			cdev_del(&hello_deves[i].dev);
		}
		kfree(hello_deves);
	}
	unregister_chrdev_region(dev_no,hello_nr);
}

int  hello_init(void)
{
	int ret,i;
	dev_t dev_no;

	hello_printk("Hello module initialise \n");
	/* alloc device number */
	if(hello_major){
		dev_no=MKDEV(hello_major,hello_minor);
		ret=register_chrdev_region(dev_no,hello_nr,"hello");		
	}
	else{
		ret=alloc_chrdev_region(&dev_no,hello_minor,hello_nr,"hello");
		hello_major=MAJOR(dev_no);	
	}	
	if(ret<0){
		hello_printk("Can't get major number %d\n",hello_major);
		return ret;
	}

	/* malloc device */
	hello_deves=kmalloc(hello_nr*sizeof(struct hello_dev),GFP_KERNEL);
	if(!hello_deves ){
		ret=-ENOMEM;
		goto  fail;
	}
	memset(hello_deves,0 ,hello_nr* sizeof(struct hello_dev));

	for (i =0;i<hello_nr;i++){
		hello_deves[i].quantum_size=quatum;
		hello_deves[i].qset_size=qset;
		hello_deves[i].size=quantum*qset;
		init_MUTEX(&hello_deves[i].sem);
		hello_setup_cdev(&hello_deves[i], i);
		init_waitqueue_head(&hello_deves[i].r_wait);
		init_waitqueue_head(&hello_deves[i].w_wait);
	}
	
	
	return 0;
	
	fail:
		hello_cleanup_module();
		return ret;

}


module_init(hello_init);
module_exit(hello_cleanup_module);

MODULE_AUTHOR("Aleon.jin");
MODULE_LICENSE("Dual BSD/GPL");

